/*
 * core.c - cve-check-tool
 *
 * Copyright (C) 2015 Intel Corporation
 *
 * cve-check-tool is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */

#define _GNU_SOURCE
#include "config.h"

#include <curl/curl.h>
#include <fcntl.h>
#include <gio/gio.h>
#include <glib.h>
#include <inttypes.h>
#include <libxml/xmlreader.h>
#include <malloc.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#include "core.h"
#include "cve-check-tool.h"

#include "cve-string.h"
#include "util.h"

#include <sqlite3.h>

const char *nvd_file = "nvd.db";
const char *nvd_dir = "NVDS";
bool use_frac_compare = false;

struct CveDB {
        /** XML traversal state */
        bool in_list;
        bool in_entry;
        bool in_product;
        bool in_summary;

        bool in_link;
        bool in_vuln_cvss;
        bool in_base_metrics;
        bool in_score;
        bool in_vector;
        bool in_date;

        xmlChar *cur_id;
        xmlChar *summary;
        xmlChar *score;
        xmlChar *vector;
        xmlChar *modified;
        GList *uris;

        /* SQL usage */
        sqlite3 *db;
        sqlite3_stmt *insert;
        sqlite3_stmt *insert_product;
        sqlite3_stmt *search_product;
        sqlite3_stmt *get_cve;
};

#define TABLE_NAME "NVD"

static bool ensure_table(CveDB *self)
{
        if (!self || !self->db) {
                return false;
        }
        int rc = 0;
        char *err = NULL;
        const char *query;

        /* If we crash we've already broken our DB anyway. */
        query = "PRAGMA synchronous = OFF;PRAGMA journal_mode = MEMORY;";
        rc = sqlite3_exec(self->db, query, NULL, NULL, &err);
        if (rc != SQLITE_OK) {
                fprintf(stderr, "ensure_table(): %s\n", err);
                sqlite3_free(err);
                return false;
        }

        query = "CREATE TABLE IF NOT EXISTS " TABLE_NAME
                " "
                "(ID TEXT UNIQUE, SUMMARY TEXT, SCORE TEXT, MODIFIED INTEGER, VECTOR TEXT);";
        rc = sqlite3_exec(self->db, query, NULL, NULL, &err);
        if (rc != SQLITE_OK) {
                fprintf(stderr, "ensure_table(): %s\n", err);
                sqlite3_free(err);
                return false;
        }

        query =
            "CREATE TABLE IF NOT EXISTS PRODUCTS (HASH INTEGER UNIQUE, ID TEXT, VENDOR TEXT, PRODUCT TEXT, VERSION "
            "TEXT);";
        rc = sqlite3_exec(self->db, query, NULL, NULL, &err);
        if (rc != SQLITE_OK) {
                fprintf(stderr, "ensure_table(): %s\n", err);
                sqlite3_free(err);
                return false;
        }
        if (err) {
                sqlite3_free(err);
        }

        return true;
}

struct cve_entry_t *cve_db_get_cve(CveDB *self, char *id)
{
        struct cve_entry_t *t = NULL;
        int rc = 0;

        if (!self || !self->db || !id) {
                return NULL;
        }

        sqlite3_reset(self->get_cve);

        printf("cve_db_get_cve: original get_cve statement is %s\n",sqlite3_sql(self->get_cve));
        if (sqlite3_bind_text(self->get_cve, 1, id, -1, SQLITE_STATIC) != SQLITE_OK) {
                fprintf(stderr, "cve_db_get_cve(): %s\n", sqlite3_errmsg(self->db));
                return NULL;
        }

        rc = sqlite3_step(self->get_cve);
        if (rc != SQLITE_ROW) {
                fprintf(stderr, "cve_db_get_cve(): %s\n", sqlite3_errmsg(self->db));
                return NULL;
        }

        // ID TEXT UNIQUE, SUMMARY TEXT, SCORE TEXT, MODIFIED INTEGER, VECTOR TEXT
        t = calloc(1, sizeof(struct cve_entry_t));
        if (!t) {
                fprintf(stderr, "cve_db_get_cve(): Memory failure\n");
                return NULL;
        }

        t->id = g_strdup((const char *)sqlite3_column_text(self->get_cve, 0));
        t->summary = g_strdup((const char *)sqlite3_column_text(self->get_cve, 1));
        t->score = g_strdup((const char *)sqlite3_column_text(self->get_cve, 2));
        t->modified = sqlite3_column_int64(self->get_cve, 3);
        t->vector = g_strdup((const char *)sqlite3_column_text(self->get_cve, 4));

        return t;
}

GList *cve_db_get_issues_frac_compare(CveDB *self, char *product, char *version)
{
        int rc = 0;
        GList *list = NULL;
        int ret = 0;

        if (!self || !self->db) {
                return NULL;
        }

        sqlite3_reset(self->search_product);

        if (sqlite3_bind_text(self->search_product, 1, product, -1, SQLITE_STATIC) != SQLITE_OK) {
                fprintf(stderr, "cve_db_get_issues_frac_compare(): %s\n", sqlite3_errmsg(self->db));
                return NULL;
        }

        while ((rc = sqlite3_step(self->search_product)) == SQLITE_ROW) {
                if ((const char *)sqlite3_column_text(self->search_product, 1) == NULL) /*skip over (null) product*/
                        continue;
                if ((const char *)sqlite3_column_text(self->search_product, 2) == NULL) /*skip over (null) version*/
                        continue;

                ret = strverscmp(version, (const char *)sqlite3_column_text(self->search_product, 2));
                if (ret <= 0) { /* our version <= NVD version */
                        list =
                            g_list_append(list, g_strdup((const char *)sqlite3_column_text(self->search_product, 0)));
                }
        }

        if (rc != SQLITE_DONE) {
                if (list) {
                        g_list_free_full(list, g_free);
                        return NULL;
                }
        }

        return list;
}

GList *cve_db_get_issues(CveDB *self, char *product, char *version)
{
        int rc = 0;
        GList *list = NULL;

        if (!self || !self->db) {
                return NULL;
        }

        printf("cve_db_get_issues: product=%s, version=%s\n",product,version);
        sqlite3_reset(self->search_product);

        /* Product */
        if (sqlite3_bind_text(self->search_product, 1, product, -1, SQLITE_STATIC) != SQLITE_OK) {
                fprintf(stderr, "cve_db_get_issues(): %s\n", sqlite3_errmsg(self->db));
                goto bail;
        }
        /* Version */
        if (sqlite3_bind_text(self->search_product, 2, version, -1, SQLITE_STATIC) != SQLITE_OK) {
                fprintf(stderr, "cve_db_get_issues(): %s\n", sqlite3_errmsg(self->db));
                goto bail;
        }

        while ((rc = sqlite3_step(self->search_product) == SQLITE_ROW)) {
                list = g_list_append(list, g_strdup((const gchar *)sqlite3_column_text(self->search_product, 0)));
        }
        if (rc != SQLITE_OK) {
                fprintf(stderr, "cve_db_get_issues(): %s\n", sqlite3_errmsg(self->db));
                if (list) {
                        g_list_free_full(list, g_free);
                        return NULL;
                }
        }
bail:
        return list;
}

static inline void free_vuln(struct vulnerability_t *t)
{
        if (!t) {
                return;
        }
        if (t->vendor) {
                g_free(t->vendor);
        }
        if (t->product) {
                g_free(t->product);
        }
        if (t->version) {
                g_free(t->version);
        }
}

/**
 * Parse a CPE line into a consumable form
 *
 * @param inp cpe:/ identifier string
 * @param vuln Where to store the resulting vulnerability data
 * @return a boolean value, true if the operation succeeded
 */
static bool parse_vuln(char *cve_id, const xmlChar *inp, struct vulnerability_t *vuln)
{
        gchar *product = NULL;
        gchar *vendor = NULL;
        gchar *version = NULL;
        autofree(gchar) *hash = NULL;

        int len = 0;
        /* Example: cpe:/a:oracle:siebel_crm:8.1.1 */
        gchar **splits = g_strsplit((const gchar *)inp, ":", 10);
        if ((len = g_strv_length(splits)) < 4) {
                g_strfreev(splits);
                return false;
        }

        vendor = g_strdup(splits[2]);
        product = g_strdup(splits[3]);
        if (len > 4) {
                version = g_strdup(splits[4]);
        }
        g_strfreev(splits);

        vuln->vendor = vendor;
        vuln->product = product;
        vuln->version = version;

        hash = g_strdup_printf("%s:%s:%s:%s", cve_id, vendor, product, version);
        if (!hash) {
                fprintf(stderr, "parse_vuln(): Out of memory\n");
                free_vuln(vuln);
                return false;
        }
        vuln->hash = g_str_hash(hash);

        return true;
}

static inline void _cve_db_clean(CveDB *self)
{
        if (self->uris) {
                g_list_free_full(self->uris, xmlFree);
                self->uris = NULL;
        }

        if (self->score) {
                xmlFree(self->score);
                self->score = NULL;
        }
        if (self->vector) {
                xmlFree(self->vector);
                self->vector = NULL;
        }
        if (self->cur_id) {
                xmlFree(self->cur_id);
                self->cur_id = NULL;
        }
        if (self->summary) {
                xmlFree(self->summary);
                self->summary = NULL;
        }
        if (self->modified) {
                xmlFree(self->modified);
                self->modified = NULL;
        }
}

/**
 * Main iterator for XML parsing
 *
 * @param r A valid xmlTextReaderPtr (open)
 */
static void process_node(CveDB *self, xmlTextReaderPtr r)
{
        const xmlChar *name = NULL;
        const xmlChar *value = NULL;
        struct vulnerability_t vuln = {.vendor = 0 };
        xmlChar *uri = NULL;
        int64_t last_mod = -1;

        name = xmlTextReaderConstName(r);
        if (!name) {
                return;
        }
        /* New entry */
        if (xmlStrEqual(name, BAD_CAST "entry")) {
                self->in_entry = !self->in_entry;
                if (!self->in_entry) {
                        int rc = 0;
                        last_mod = parse_xml_date((char *)self->modified);

                        sqlite3_reset(self->insert);

                        /* ID */
                        if (sqlite3_bind_text(self->insert, 1, (const char *)self->cur_id, -1, SQLITE_STATIC) !=
                            SQLITE_OK) {
                                fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                                goto next;
                        }
                        /* SUMMARY */
                        if (sqlite3_bind_text(self->insert, 2, (const char *)self->summary, -1, SQLITE_STATIC) !=
                            SQLITE_OK) {
                                fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                                goto next;
                        }
                        /* SCORE */
                        if (sqlite3_bind_text(self->insert, 3, (const char *)self->score, -1, SQLITE_STATIC) !=
                            SQLITE_OK) {
                                fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                                goto next;
                        }
                        /* MODIFIED */
                        if (sqlite3_bind_int64(self->insert, 4, last_mod)) {
                                fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                                goto next;
                        }
                        /* VECTOR */
                        if (sqlite3_bind_text(self->insert, 5, (const char *)self->vector, -1, SQLITE_STATIC) !=
                            SQLITE_OK) {
                                fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                                goto next;
                        }

                        rc = sqlite3_step(self->insert);
                        if (rc != SQLITE_DONE) {
                                fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                        }
                next:
                        _cve_db_clean(self);
                        return;
                }
                if (self->cur_id) {
                        xmlFree(self->cur_id);
                }
                self->cur_id = xmlTextReaderGetAttribute(r, BAD_CAST "id");
                return;
        }
        if (xmlStrEqual(name, BAD_CAST "vuln:references")) {
                self->in_link = !self->in_link;
                return;
        }
        if (self->in_link && xmlStrEqual(name, BAD_CAST "vuln:reference")) {
                uri = xmlTextReaderGetAttribute(r, BAD_CAST "href");
                if (!uri) {
                        return;
                }
                self->uris = g_list_append(self->uris, uri);
                uri = NULL;
        }
        if (xmlStrEqual(name, BAD_CAST "vuln:vulnerable-software-list")) {
                self->in_list = !self->in_list;
                return;
        }
        if (self->in_list && xmlStrEqual(name, BAD_CAST "vuln:product")) {
                self->in_product = !self->in_product;
                return;
        }
        /* Score checking */
        if (xmlStrEqual(name, BAD_CAST "vuln:cvss")) {
                self->in_vuln_cvss = !self->in_vuln_cvss;
                return;
        }
        if (self->in_vuln_cvss && xmlStrEqual(name, BAD_CAST "cvss:base_metrics")) {
                self->in_base_metrics = !self->in_base_metrics;
                return;
        }
        if (self->in_base_metrics && xmlStrEqual(name, BAD_CAST "cvss:score")) {
                self->in_score = !self->in_score;
        }
        if (self->in_base_metrics && xmlStrEqual(name, BAD_CAST "cvss:access-vector")) {
                self->in_vector = !self->in_vector;
        }
        if (self->in_base_metrics && self->in_vector) {
                value = xmlTextReaderConstValue(r);
                if (!value) {
                        return;
                }
                self->vector = xmlStrdup(value);
        }
        if (self->in_base_metrics && self->in_score) {
                value = xmlTextReaderConstValue(r);
                if (!value) {
                        return;
                }
                self->score = xmlStrdup(value);
        }
        /* Get last modified */
        if (xmlStrEqual(name, BAD_CAST "vuln:last-modified-datetime")) {
                self->in_date = !self->in_date;
        }
        if (self->in_date) {
                value = xmlTextReaderConstValue(r);
                if (!value) {
                        return;
                }
                self->modified = xmlStrdup(value);
        }
        /* Product checking */
        if (self->in_list && self->in_product) {
                value = xmlTextReaderConstValue(r);
                int rc = 0;

                if (!value) {
                        return;
                }
                if (!parse_vuln((char *)self->cur_id, value, &vuln)) {
                        return;
                }

                sqlite3_reset(self->insert_product);

                /* HASH */
                if (sqlite3_bind_int(self->insert_product, 1, vuln.hash) != SQLITE_OK) {
                        fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                        goto end_product;
                }

                /* ID */
                if (sqlite3_bind_text(self->insert_product, 2, (const char *)self->cur_id, -1, SQLITE_STATIC) !=
                    SQLITE_OK) {
                        fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                        goto end_product;
                }
                /* VENDOR */
                if (sqlite3_bind_text(self->insert_product, 3, vuln.vendor, -1, SQLITE_STATIC) != SQLITE_OK) {
                        fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                        goto end_product;
                }
                /* PRODUCT */
                if (sqlite3_bind_text(self->insert_product, 4, vuln.product, -1, SQLITE_STATIC) != SQLITE_OK) {
                        fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                        goto end_product;
                }
                /* VERSION */
                if (sqlite3_bind_text(self->insert_product, 5, vuln.version, -1, SQLITE_STATIC) != SQLITE_OK) {
                        fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                        goto end_product;
                }

                /* Commit product. */
                rc = sqlite3_step(self->insert_product);
                if (rc != SQLITE_DONE) {
                        fprintf(stderr, "process_node(): %s\n", sqlite3_errmsg(self->db));
                }

        end_product:
                free_vuln(&vuln);
                return;
        }
        if (self->in_entry && xmlStrEqual(name, BAD_CAST "vuln:summary")) {
                self->in_summary = !self->in_summary;
                if (self->in_summary && self->summary) {
                        xmlFree(self->summary);
                        self->summary = NULL;
                }
                return;
        }
        if (self->in_summary) {
                self->summary = xmlTextReaderValue(r);
                return;
        }
}

/**
 * Parse an NVD xml database
 *
 * @param fname Path to the nvd db
 * @return a boolean value, true if the operation succeeded
 */
bool cve_db_load(CveDB *self, const char *fname)
{
        bool b = false;
        __attribute__((unused)) int rc;

        if (!self || !fname) {
                return false;
        }
        int fd = 0;

        fd = open(fname, O_RDONLY);
        if (fd < 0) {
                return false;
        }
        /* If it fails, it fails */
        rc = posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);

        xmlTextReaderPtr r = xmlReaderForFd(fd, fname, NULL, 0);
        if (!r) {
                close(fd);
                return false;
        }
        int ret;

        while ((ret = xmlTextReaderRead(r)) > 0) {
                process_node(self, r);
        }

        b = true;
#ifdef HAVE_MALLOC_TRIM
        malloc_trim(0);
#endif
        xmlFreeTextReader(r);
        if (fd) {
                close(fd);
        }

        return b;
}

bool cve_db_finalize(CveDB *self)
{
        int rc;
        const char *query;

        rc = sqlite3_exec(self->db, "END TRANSACTION;", NULL, NULL, NULL);
        if (rc != SQLITE_OK) {
                fprintf(stderr, "cve_db_finalize(): %s\n", sqlite3_errmsg(self->db));
                return false;
        }

        query = "CREATE INDEX IF NOT EXISTS PRODUCT_IDX ON PRODUCTS (PRODUCT, VERSION);";
        rc = sqlite3_exec(self->db, query, NULL, NULL, NULL);
        if (rc != SQLITE_OK) {
                fprintf(stderr, "cve_db_finalize(): %s\n", sqlite3_errmsg(self->db));
                return false;
        }
        return true;
}

bool cve_db_begin(CveDB *self)
{
        int rc;

        rc = sqlite3_exec(self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
        if (rc != SQLITE_OK) {
                fprintf(stderr, "cve_db_begin(): %s\n", sqlite3_errmsg(self->db));
                return false;
        }
        return true;
}

CveDB *cve_db_new(const char *path)
{
        CveDB *ret = NULL;
        sqlite3 *r = NULL;
        int rc = 0;
        const char *q;
        sqlite3_stmt *stm = NULL;

        ret = calloc(1, sizeof(struct CveDB));
        if (!ret) {
                fprintf(stdout, "cve_db_new: Out of memory\n");
                return NULL;
        }

        rc = sqlite3_open(path, &r);
        if (rc != SQLITE_OK) {
                if (r) {
                        sqlite3_close(r);
                }
                free(ret);
                return NULL;
        }
        ret->db = r;

        if (!ensure_table(ret)) {
                fprintf(stderr, "cve_db_new(): Table construction failure\n");
                cve_db_free(ret);
                return NULL;
        }

        q = "INSERT OR REPLACE INTO " TABLE_NAME " VALUES (?, ?, ?, ?, ?);";

        rc = sqlite3_prepare_v2(ret->db, q, -1, &stm, NULL);
        if (rc != SQLITE_OK) {
                fprintf(stderr, "cve_db_new(): %s\n", sqlite3_errmsg(ret->db));
                cve_db_free(ret);
                return NULL;
        }
        ret->insert = stm;
        stm = NULL;

        /* Insert product. */
        q = "INSERT OR REPLACE INTO PRODUCTS VALUES (?, ?, ?, ?, ?)";
        rc = sqlite3_prepare_v2(ret->db, q, -1, &stm, NULL);
        if (rc != SQLITE_OK) {
                fprintf(stderr, "cve_db_new(): %s\n", sqlite3_errmsg(ret->db));
                cve_db_free(ret);
                return NULL;
        }
        ret->insert_product = stm;
        stm = NULL;

        /* Search product. */
        if (use_frac_compare) {
                q = "select ID, PRODUCT, VERSION from PRODUCTS where PRODUCT = ?";
        } else {
                q = "SELECT ID FROM PRODUCTS WHERE PRODUCT = ? AND VERSION = ? COLLATE NOCASE";
        }
        rc = sqlite3_prepare_v2(ret->db, q, -1, &stm, NULL);
        if (rc != SQLITE_OK) {
                fprintf(stderr, "cve_db_new(): %s\n", sqlite3_errmsg(ret->db));
                cve_db_free(ret);
                return NULL;
        }
        ret->search_product = stm;
        stm = NULL;

        /* Get CVE. */
        q = "SELECT * FROM NVD WHERE ID = ?";
        rc = sqlite3_prepare_v2(ret->db, q, -1, &stm, NULL);
        if (rc != SQLITE_OK) {
                fprintf(stderr, "cve_db_new(): %s\n", sqlite3_errmsg(ret->db));
                cve_db_free(ret);
                return NULL;
        }
        ret->get_cve = stm;
        stm = NULL;

        return ret;
}

void cve_db_free(CveDB *self)
{
        if (!self) {
                return;
        }
        if (self->insert) {
                sqlite3_finalize(self->insert);
        }
        if (self->insert_product) {
                sqlite3_finalize(self->insert_product);
        }
        if (self->search_product) {
                sqlite3_finalize(self->search_product);
        }
        if (self->get_cve) {
                sqlite3_finalize(self->get_cve);
        }
        if (self->db) {
                sqlite3_close(self->db);
        }
        _cve_db_clean(self);
        free(self);
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 8
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * vi: set shiftwidth=8 tabstop=8 expandtab:
 * :indentSize=8:tabSize=8:noTabs=true:
 */
